<html>
 <head>
  <meta charset="UTF-8">
 </head>
 <body>
  <h1 data-lake-id="bR0ha" id="bR0ha"><span data-lake-id="u4475c994" id="u4475c994">典型回答</span></h1>
  <p data-lake-id="uf94dce60" id="uf94dce60"><br></p>
  <p data-lake-id="uf667c7ef" id="uf667c7ef"><strong><span data-lake-id="u9d24d5e3" id="u9d24d5e3">同步容器是通过加锁实现线程安全的，并且只能保证单独的操作是线程安全的，无法保证复合操作的线程安全性。并且同步容器的读和写操作之间会互相阻塞。</span></strong></p>
  <p data-lake-id="ub2e93bfe" id="ub2e93bfe"><span data-lake-id="u3dc58a73" id="u3dc58a73">​</span><br></p>
  <p data-lake-id="udfca7d72" id="udfca7d72"><span data-lake-id="u344c181c" id="u344c181c">在多线程场景中，如果使用同步容器，一定要注意复合操作的线程安全问题。必要时候要主动加锁。在并发场景中，建议直接使用java.util.concurent包中提供的容器类，如果需要复合操作时，建议使用有些容器自身提供的复合方法。</span></p>
  <p data-lake-id="u279862a5" id="u279862a5"><span data-lake-id="u2b365107" id="u2b365107">​</span><br></p>
  <p data-lake-id="ued5f9c4f" id="ued5f9c4f"><span data-lake-id="u73d52334" id="u73d52334">什么是同步容器？最常见的同步容器就是Vector和Hashtable了，在Java中，同步容器主要包括2类：</span></p>
  <ul list="u0dec059d">
   <li fid="uc79de6cc" data-lake-id="uf4ac4f14" id="uf4ac4f14" data-lake-index-type="true"><span data-lake-id="uc2e78571" id="uc2e78571">1、Vector、Stack、HashTable</span></li>
   <li fid="uc79de6cc" data-lake-id="u8c983457" id="u8c983457" data-lake-index-type="true"><span data-lake-id="uc7d8d748" id="uc7d8d748">2、Collections类中提供的静态工厂方法创建的类</span></li>
  </ul>
  <p data-lake-id="ud2370d45" id="ud2370d45"><br></p>
  <p data-lake-id="u6e4a47e5" id="u6e4a47e5"><span data-lake-id="ubd8fac73" id="ubd8fac73">本文拿相对简单的Vecotr来举例，我们先来看下Vector中几个重要方法的源码：</span></p>
  <p data-lake-id="ufc4dd991" id="ufc4dd991" style="text-align: justify"><span data-lake-id="uf04f908b" id="uf04f908b" class="lake-fontsize-12" style="color: rgb(51, 51, 51)">​</span><br></p>
  <pre lang="java"><code>
public synchronized boolean add(E e) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}

public synchronized E remove(int index) {
    modCount++;
    if (index &gt;= elementCount)
        throw new ArrayIndexOutOfBoundsException(index);
    E oldValue = elementData(index);

    int numMoved = elementCount - index - 1;
    if (numMoved &gt; 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--elementCount] = null; // Let gc do its work

    return oldValue;
}

public synchronized E get(int index) {
    if (index &gt;= elementCount)
        throw new ArrayIndexOutOfBoundsException(index);

    return elementData(index);
}
</code></pre>
  <p data-lake-id="u034b969e" id="u034b969e"><br></p>
  <p data-lake-id="u893ec13e" id="u893ec13e"><span data-lake-id="u47eed6f0" id="u47eed6f0">可以看到，Vector这样的同步容器的所有公有方法全都是synchronized的，也就是说，我们可以在多线程场景中放心的使用单独这些方法，因为这些方法本身的确是线程安全的。</span></p>
  <p data-lake-id="uba352019" id="uba352019"><span data-lake-id="u44d5d0ae" id="u44d5d0ae">​</span><br></p>
  <p data-lake-id="u93fc3720" id="u93fc3720"><span data-lake-id="uc13f750e" id="uc13f750e">但是，请注意上面这句话中，有一个比较关键的词：</span><strong><span data-lake-id="u03732d0e" id="u03732d0e">单独</span></strong></p>
  <p data-lake-id="ud71493c2" id="ud71493c2"><span data-lake-id="ua5bc6e94" id="ua5bc6e94">​</span><br></p>
  <p data-lake-id="uc1071317" id="uc1071317"><span data-lake-id="u5894d5e0" id="u5894d5e0">因为，虽然同步容器的所有方法都加了锁，但是对这些容器的复合操作无法保证其线程安全性。需要客户端通过主动加锁来保证。</span></p>
  <p data-lake-id="u0afc9adc" id="u0afc9adc"><span data-lake-id="ub3653234" id="ub3653234">​</span><br></p>
  <p data-lake-id="u41314ac6" id="u41314ac6"><span data-lake-id="u31182c21" id="u31182c21">简单举一个例子，我们定义如下删除Vector中最后一个元素方法：</span></p>
  <p data-lake-id="ue852c838" id="ue852c838"><span data-lake-id="u68d35e5e" id="u68d35e5e">​</span><br></p>
  <pre lang="java"><code>
public Object deleteLast(Vector v){
    int lastIndex  = v.size()-1;
    v.remove(lastIndex);
}
</code></pre>
  <p data-lake-id="ud549332a" id="ud549332a"><br></p>
  <p data-lake-id="u9279e44e" id="u9279e44e"><span data-lake-id="u3e4b1404" id="u3e4b1404">上面这个方法是一个复合方法，包括size(）和remove()，乍一看上去好像并没有什么问题，无论是size()方法还是remove()方法都是线程安全的，那么整个deleteLast方法应该也是线程安全的。</span></p>
  <p data-lake-id="u3162e4cc" id="u3162e4cc"><span data-lake-id="u3e2f456e" id="u3e2f456e">​</span><br></p>
  <p data-lake-id="uf305dc31" id="uf305dc31"><span data-lake-id="u59974e68" id="u59974e68">但是时，如果多线程调用该方法的过程中，remove方法有可能抛出ArrayIndexOutOfBoundsException。</span></p>
  <p data-lake-id="u58e77138" id="u58e77138"><br></p>
  <pre lang="java"><code>
Exception in thread "Thread-1" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 879
    at java.util.Vector.remove(Vector.java:834)
    at com.hollis.Test.deleteLast(EncodeTest.java:40)
    at com.hollis.Test$2.run(EncodeTest.java:28)
    at java.lang.Thread.run(Thread.java:748)
</code></pre>
  <p data-lake-id="ua26d3752" id="ua26d3752"><br></p>
  <p data-lake-id="ucf206390" id="ucf206390"><span data-lake-id="u1095d6e7" id="u1095d6e7">我们上面贴了remove的源码，我们可以分析得出：当index &gt;= elementCount时，会抛出ArrayIndexOutOfBoundsException ，也就是说，当当前索引值不再有效的时候，将会抛出这个异常。</span></p>
  <p data-lake-id="u94fd82c4" id="u94fd82c4"><span data-lake-id="ue12c11ef" id="ue12c11ef">​</span><br></p>
  <p data-lake-id="u4fc0999e" id="u4fc0999e"><span data-lake-id="uaffcabe3" id="uaffcabe3">因为removeLast方法，有可能被多个线程同时执行，当线程2通过index()获得索引值为10，在尝试通过remove()删除该索引位置的元素之前，线程1把该索引位置的值删除掉了，这时线程一在执行时便会抛出异常。</span></p>
  <p data-lake-id="ua023d6df" id="ua023d6df"><span data-lake-id="uc63d8ea0" id="uc63d8ea0">​</span><br></p>
  <p data-lake-id="u769c1772" id="u769c1772"><img src="https://cdn.nlark.com/yuque/0/2024/jpeg/5378072/1705728607475-03116499-bc2e-44ec-89dc-9c96b919fd5a.jpeg?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_21%2Ctext_SmF2YSA4IEd1IFA%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10"></p>
  <p data-lake-id="u96d344dc" id="u96d344dc"><span data-lake-id="uae5069ab" id="uae5069ab"><br></span></p>
  <p data-lake-id="uc5e0b17e" id="uc5e0b17e"><span data-lake-id="uecb97c39" id="uecb97c39">为了避免出现类似问题，可以尝试加锁：</span></p>
  <p data-lake-id="u3b3a486d" id="u3b3a486d"><span data-lake-id="udf40c697" id="udf40c697">​</span><br></p>
  <pre lang="java"><code>
public void deleteLast() {
    synchronized (v) {
        int index = v.size() - 1;
        v.remove(index);
    }
}
</code></pre>
  <p data-lake-id="ue991106c" id="ue991106c"><br></p>
  <p data-lake-id="u0c849efa" id="u0c849efa"><span data-lake-id="u228b6b01" id="u228b6b01">如上，我们在deleteLast中，对v进行加锁，即可保证同一时刻，不会有其他线程删除掉v中的元素。</span></p>
  <p data-lake-id="uaa757399" id="uaa757399"><br></p>
  <p data-lake-id="u75531b01" id="u75531b01"><span data-lake-id="uac305b26" id="uac305b26">另外，如果以下代码会被多线程执行时，也要特别注意：</span></p>
  <p data-lake-id="uc80ad694" id="uc80ad694"><span data-lake-id="ucaa009e1" id="ucaa009e1">​</span><br></p>
  <pre lang="java"><code>
for (int i = 0; i &lt; v.size(); i++) {
    v.remove(i);
}
</code></pre>
  <p data-lake-id="u27b26d9d" id="u27b26d9d"><br></p>
  <p data-lake-id="u48169e81" id="u48169e81"><span data-lake-id="u199a189b" id="u199a189b">由于，不同线程在同一时间操作同一个Vector，其中包括删除操作，那么就同样有可能发生线程安全问题。所以，在使用同步容器的时候，如果涉及到多个线程同时执行删除操作，就要考虑下是否需要加锁。</span></p>
  <p data-lake-id="ufb4d73ee" id="ufb4d73ee"><span data-lake-id="u839340ca" id="u839340ca">​</span><br></p>
  <h3 data-lake-id="rquwF" id="rquwF"><span data-lake-id="uf923bfad" id="uf923bfad">同步容器的问题</span></h3>
  <p data-lake-id="u2fa38d89" id="u2fa38d89"><span data-lake-id="u174f395c" id="u174f395c">前面说过了，同步容器直接保证耽搁操作的线程安全性，但是无法保证复合操作的线程安全，遇到这种情况时，必须要通过主动加锁的方式来实现。</span></p>
  <p data-lake-id="u8f7877cd" id="u8f7877cd"><span data-lake-id="u615d1bbf" id="u615d1bbf">​</span><br></p>
  <p data-lake-id="u2afc59dc" id="u2afc59dc"><span data-lake-id="ube67ae0e" id="ube67ae0e">而且，除此之外，同步容易由于对其所有方法都加了锁，这就导致多个线程访问同一个容器的时候，只能进行顺序访问，即使是不同的操作，也要排队，如get和add要排队执行。这就大大的降低了容器的并发能力。</span></p>
  <p data-lake-id="u80cb734f" id="u80cb734f"><span data-lake-id="ub2ac0361" id="ub2ac0361">​</span><br></p>
  <h3 data-lake-id="Xcx4Y" id="Xcx4Y" style="text-align: justify"><span data-lake-id="u22d714e1" id="u22d714e1" style="color: rgb(51, 51, 51)">并发容器</span></h3>
  <p data-lake-id="u73952e3e" id="u73952e3e" style="text-align: justify"><span data-lake-id="uc91fc3a6" id="uc91fc3a6" class="lake-fontsize-12" style="color: rgb(51, 51, 51)">针对前文提到的同步容器存在的并发度低问题，从Java5开始，java.util.concurent包下，提供了大量支持高效并发的访问的集合类，我们称之为并发容器。</span></p>
  <p data-lake-id="ucff2af13" id="ucff2af13" style="text-align: justify"><span data-lake-id="u9ab8fd77" id="u9ab8fd77" class="lake-fontsize-12" style="color: rgb(51, 51, 51)">​</span><br></p>
  <p data-lake-id="ub7c8bc99" id="ub7c8bc99" style="text-align: justify"><img src="https://cdn.nlark.com/yuque/0/2024/jpeg/5378072/1705728673343-b632dcc9-3770-4abd-84e2-e929c43472a4.jpeg?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_9%2Ctext_SmF2YSA4IEd1IFA%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10"></p>
  <p data-lake-id="ud734cafa" id="ud734cafa" style="text-align: justify"><span data-lake-id="uddd63579" id="uddd63579" class="lake-fontsize-12" style="color: rgb(51, 51, 51)">​</span><br></p>
  <p data-lake-id="u55038d7d" id="u55038d7d"><span data-lake-id="uec89e88f" id="uec89e88f">针对前文提到的同步容器的复合操作的问题，一般在Map中发生的比较多，所以在ConcurrentHashMap中增加了对常用复合操作的支持，比如"若没有则添加"：putIfAbsent()，替换：replace()。这2个操作都是原子操作，可以保证线程安全。</span></p>
  <p data-lake-id="u8aae92d6" id="u8aae92d6"><span data-lake-id="u11b02b66" id="u11b02b66">​</span><br></p>
  <p data-lake-id="ue22eda80" id="ue22eda80"><span data-lake-id="ude3fb91d" id="ude3fb91d">​</span><br></p>
  <p data-lake-id="ua23674a7" id="ua23674a7"><span data-lake-id="u53cd6781" id="u53cd6781">另外，并发包中的CopyOnWriteArrayList和CopyOnWriteArraySet是Copy-On-Write的两种实现。</span></p>
  <p data-lake-id="ue6b4271a" id="ue6b4271a"><span data-lake-id="u9956cca6" id="u9956cca6">​</span><br></p>
  <p data-lake-id="ubd67afa3" id="ubd67afa3"><span data-lake-id="u56950cb8" id="u56950cb8">​</span><br></p>
  <p data-lake-id="u4801d2fa" id="u4801d2fa"><span data-lake-id="ubb75ff1b" id="ubb75ff1b">Copy-On-Write容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候，不直接往当前容器添加，而是先将当前容器进行Copy，复制出一个新的容器，然后新的容器里添加元素，添加完元素之后，再将原容器的引用指向新的容器。</span></p>
  <p data-lake-id="u2513ad67" id="u2513ad67"><span data-lake-id="ubc823f2e" id="ubc823f2e">​</span><br></p>
  <p data-lake-id="ua40591e0" id="ua40591e0"><span data-lake-id="ueae05cd2" id="ueae05cd2">CopyOnWriteArrayList中add/remove等写方法是需要加锁的，而读方法是没有加锁的。</span></p>
  <p data-lake-id="u5cca116d" id="u5cca116d"><span data-lake-id="ub3624af1" id="ub3624af1">​</span><br></p>
  <p data-lake-id="u4b55b20c" id="u4b55b20c"><span data-lake-id="u600d2597" id="u600d2597">这样做的好处是我们可以对CopyOnWrite容器进行并发的读，当然，这里读到的数据可能不是最新的。因为写时复制的思想是通过延时更新的策略来实现数据的最终一致性的，并非强一致性。</span></p>
  <p data-lake-id="u766ca1b6" id="u766ca1b6"><span data-lake-id="ud12ed8f8" id="ud12ed8f8">​</span><br></p>
  <p data-lake-id="uf8f7b571" id="uf8f7b571"><span data-lake-id="u2acc3a61" id="u2acc3a61">但是，作为代替Vector的CopyOnWriteArrayList并没有解决同步容器的复合操作的线程安全性问题。</span></p>
 </body>
</html>